/***************************************************************************************
* Copyright (c) 2014-2022 Zihao Yu, Nanjing University
*
* NEMU is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*          http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
*
* See the Mulan PSL v2 for more details.
***************************************************************************************/

#include <dlfcn.h>
#include "difftest.h"

#include "../cpu/cpu.h"
#include "../memory/mem.h"

#include <readline/readline.h>
CPU_state cpu_state;

#include "difftest-def.h"

void (*ref_difftest_memcpy)(paddr_t addr, void *buf, size_t n, bool direction) = NULL;
void (*ref_difftest_regcpy)(void *dut, bool direction) = NULL;
void (*ref_difftest_exec)(uint64_t n) = NULL;
void (*ref_difftest_raise_intr)(uint64_t NO) = NULL;

#ifdef CONFIG_DIFFTEST

static bool is_skip_ref = false;
static int skip_dut_nr_inst = 0;

// this is used to let ref skip instructions which
// can not produce consistent behavior with NEMU
void difftest_skip_ref() {
  is_skip_ref = true;
  // If such an instruction is one of the instruction packing in QEMU
  // (see below), we end the process of catching up with QEMU's pc to
  // keep the consistent behavior in our best.
  // Note that this is still not perfect: if the packed instructions
  // already write some memory, and the incoming instruction in NEMU
  // will load that memory, we will encounter false negative. But such
  // situation is infrequent.
  skip_dut_nr_inst = 0;
}

// this is used to deal with instruction packing in QEMU.
// Sometimes letting QEMU step once will execute multiple instructions.
// We should skip checking until NEMU's pc catches up with QEMU's pc.
// The semantic is
//   Let REF run `nr_ref` instructions first.
//   We expect that DUT will catch up with REF within `nr_dut` instructions.
void difftest_skip_dut(int nr_ref, int nr_dut) {
  skip_dut_nr_inst += nr_dut;

  while (nr_ref -- > 0) {
    ref_difftest_exec(1);
  }
}

void init_difftest(char *ref_so_file, long img_size, int port) {
  assert(ref_so_file != NULL);

  void *handle;
  handle = dlopen(ref_so_file, RTLD_LAZY);
  assert(handle);
  
  ref_difftest_memcpy = (void(*)(paddr_t,void *,size_t,bool))dlsym(handle, "difftest_memcpy");
  assert(ref_difftest_memcpy);

  ref_difftest_regcpy = (void(*)(void*,bool))dlsym(handle, "difftest_regcpy");
  assert(ref_difftest_regcpy);

  ref_difftest_exec = (void(*)(uint64_t))dlsym(handle, "difftest_exec");
  assert(ref_difftest_exec);

  ref_difftest_raise_intr = (void(*)(uint64_t))dlsym(handle, "difftest_raise_intr");
  assert(ref_difftest_raise_intr);

  void (*ref_difftest_init)(int) = (void(*)(int))dlsym(handle, "difftest_init");
  assert(ref_difftest_init);

  Log("Differential testing: %s", ANSI_FMT("ON", ANSI_FG_GREEN) );
  Log("The result of every instruction will be compared with %s. "
      "This will help you a lot for debugging, but also significantly reduce the performance. "
      "If it is not necessary, you can turn it off in menuconfig.", ref_so_file);

  ref_difftest_init(port);
  ref_difftest_memcpy(RESET_VECTOR, guest_to_host(RESET_VECTOR), CONFIG_MSIZE, DIFFTEST_TO_REF);
  ref_difftest_regcpy(&cpu_state, DIFFTEST_TO_REF);

}

extern const char *regs_name[];
static void checkregs(CPU_state *ref, vaddr_t pc) {
  if (!isa_difftest_checkregs(ref, pc)) {
    printf("error in PC : 0x%016lx\n",pc);
    // printf("last dut:\n");
    // printf("PC:0x%016lx\n",last.pc);
    // isa_reg_display(&last);
    printf("dut:\n");
    printf("PC:0x%016lx\n",cpu_state.pc);
    isa_reg_display(&cpu_state);
    printf("ref:\n");
    printf("PC:0x%016lx\n",ref->pc);
    for (int i = 0; i < 32; i++)
    { 
      char *fmt = REG_FMT;
      if(ref->gpr[i] != cpu_state.gpr[i]) fmt = ANSI_FMT(REG_FMT, ANSI_FG_RED);
        printf(fmt,regs_name[i], ref->gpr[i]);
        if (i % 4 == 3)
            printf("\n");
    }
    IFONE(CONFIG_MTRACE, mtracer.print());
    IFONE(CONFIG_ITRACE, itracer.print());
    continue_flag = false;
  }
}
static bool intr_flag = false; 
static word_t cause;
void difftest_raise_intr_ref(word_t NO)
{
  cause = NO;
  intr_flag = true;
}
static word_t last_PC_ref;
#define update_intr() ref_difftest_regcpy(&ref_r, DIFFTEST_TO_DUT);\
  if(intr_flag) \
  {\
    ref_r.pc = last_PC_ref;\
    ref_difftest_regcpy(&ref_r, DIFFTEST_TO_REF);\
    intr_flag = false;\
    ref_difftest_raise_intr(cause);\
    ref_difftest_regcpy(&ref_r, DIFFTEST_TO_DUT);\
  }\
  last_PC_ref = ref_r.pc;  // itracer.save(-1,ref_r.pc);

void difftest_step(vaddr_t pc, vaddr_t npc) {
  CPU_state ref_r;

  if (skip_dut_nr_inst > 0) {
    LogErr("skip_dut_nr_inst:%d",skip_dut_nr_inst);
    ref_difftest_regcpy(&ref_r, DIFFTEST_TO_DUT);
    if (ref_r.pc == npc) {
      skip_dut_nr_inst = 0;
      checkregs(&ref_r, npc);
      return;
    }
    skip_dut_nr_inst --;
    if (skip_dut_nr_inst == 0)
      panic("can not catch up with ref.pc = " FMT_WORD " at pc = " FMT_WORD "\n", ref_r.pc, pc);
    return;
  }

  if(intr_flag)
  {
    intr_flag = false;
    ref_difftest_raise_intr(cause);
    ref_difftest_regcpy(&ref_r, DIFFTEST_TO_DUT);
    checkregs(&ref_r, npc);
    return;
  }

  if (is_skip_ref) {
    // to skip the checking of an instruction, just copy the reg state to reference design
    ref_difftest_regcpy(&cpu_state, DIFFTEST_TO_REF);
    is_skip_ref = false;
    // update_intr();
    return;
  }

  ref_difftest_exec(1);
  // update_intr();
  ref_difftest_regcpy(&ref_r, DIFFTEST_TO_DUT);
  // IFONE(CONFIG_ITRACE, ITracer_Data data; data.pc = ref_r.pc; data.instruction = -1; itracer.save(data));
  checkregs(&ref_r, npc);
}
#else
void init_difftest(char *ref_so_file, long img_size, int port) { }
#endif
